Skip to content

feat(store): billing subscription and checkout reads skip soft-deleted rows - #1963

Merged
rohilsurana merged 4 commits into
mainfrom
soft-delete-billing-subscriptions-checkouts
Oct 1, 2026
Merged

rohilsurana merged 4 commits into
mainfrom
soft-delete-billing-subscriptions-checkouts

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

The billing subscription and checkout repositories read rows without checking deleted_at. They now skip deleted rows, using the same live and fromLive helpers as the rest of the store.

These are the last two of the five repositories the org delete cascade touches. #1962 did the other three and has merged, so with this change all five filter. Nothing writes deleted_at to either table yet, so every read returns the same rows as before.

Changes

  • Subscriptions: GetByID, GetByName, GetByProviderID and List now read from fromLive.
  • Checkouts: GetByID, GetByName and List now read from fromLive.
  • A new postgres-backed test suite for each.

Technical Details

The two correlated subqueries on billing_customers in the subscription repository are left alone on purpose. They only fill the org id and customer name into the RETURNING clause of Create and UpdateByID, for the audit record. Filtering them would do more than blank two columns: both fields are plain strings, so a NULL would fail the struct scan and abort the whole write. An audit record should say which org a subscription belonged to even after the customer is deleted.

GetByName on both repositories is unreachable. Neither table has a name column and neither method has a caller, so calling either fails with a postgres error before deleted_at matters. I filtered them to keep the files consistent, but they are dead code and deleting them would be a fair follow-up.

The update paths and the hard deletes are untouched, matching #1962.

No migration. Subscriptions have carried deleted_at since the table was created, and checkouts got it in 20260916100000_soft_delete_columns.

Test Plan

  • go test -run 'TestBillingSubscriptionRepositoryPG|TestBillingCheckoutRepositoryPG' ./internal/store/postgres/ passes
  • Both suites fail on main
  • golangci-lint run ./internal/store/postgres/... reports no issues
  • go test ./internal/store/postgres/ ./billing/... passes

Each suite seeds a live row and a deleted one, and I watched all five tests fail before adding the filters. Reverting the seven fromLive calls was also checked: every test fails, and each test exercises exactly one read, so each one catches its own change.

SQL Safety (if your PR touches *_repository.go or goqu.*)

  • Values flow through ? placeholders, goqu.Ex{}, or goqu.Record{} — never fmt.Sprintf or + building a query that gets executed.
  • ToSQL() callers capture and forward params (query, params, err := stmt.ToSQL(); db.…Context(ctx, …, query, params...)). Never query, _, err := ….
  • No ? placeholders inside single-quoted SQL literals in goqu.L (use make_interval(hours => ?)-style functions instead).
  • Any //nolint:forbidigo or // #nosec G20x annotation has a one-line justification on the same line that a reviewer can verify.

The added predicates come from the existing live() helper and bind no values. The only fmt.Sprintf in the diff builds TRUNCATE in the test teardowns from package table constants, the same as the other suites here.

@vercel

vercel Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Oct 1, 2026 10:16am UTC

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 7f070495-1d77-4815-92af-26872a3d4837

📥 Commits

Reviewing files that changed from the base of the PR and between d568781 and d2efb05.

📒 Files selected for processing (3)
  • core/deleter/service.go
  • internal/store/postgres/billing_checkout_repository.go
  • internal/store/postgres/billing_subscription_repository.go
💤 Files with no reviewable changes (2)
  • internal/store/postgres/billing_checkout_repository.go
  • internal/store/postgres/billing_subscription_repository.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Billing checkout and subscription lookups and lists now exclude soft-deleted records. Deleted checkouts and subscriptions are reported as not found when retrieved by ID; deleted subscriptions are also reported as not found when retrieved by provider ID. This keeps deleted billing records out of active results while preserving clear not-found responses for direct lookups.

Walkthrough

Billing checkout and subscription read queries now use fromLive. The repositories no longer provide GetByName. New PostgreSQL integration tests check reads of live and soft-deleted records.

Changes

Billing repository live reads

Layer / File(s) Summary
Checkout live reads
internal/store/postgres/billing_checkout_repository.go, internal/store/postgres/billing_checkout_repository_pg_test.go
GetByID and List use fromLive; GetByName was removed. PostgreSQL tests verify that reads return live checkouts and omit soft-deleted checkouts.
Subscription live reads
internal/store/postgres/billing_subscription_repository.go, internal/store/postgres/billing_subscription_repository_pg_test.go, core/deleter/service.go
GetByID, GetByProviderID, and List use fromLive; GetByName was removed. PostgreSQL tests verify that reads return live subscriptions and omit soft-deleted subscriptions. A TODO documents deletion concerns involving soft-deleted records.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: amangit07, whoabhisheksah

Merge Risk: ⚪ Minimal · up to d2efb

Billing reads now exclude soft-deleted subscriptions and checkouts. No concrete merge-blocking issue is established; the change is mergeable subject to normal checks.

Architecture Summary

Architecture risk: 🔵 Low · up to d2efb

The change affects 2 systems.

Changed systems: core, internal

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — core (service) was modified; 1 changed file maps to changed impact.
  • observed — internal (service) was modified; 4 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in internal/store/postgres/billing_checkout_repository_pg_test.go: Adds the PostgreSQL test suite and lifecycle setup: it opens a test client, constructs the repository, and closes the client during teardown, failing the test on setup or close errors.
  • observed — Modified behavior in internal/store/postgres/billing_checkout_repository_pg_test.go: Seeds a customer and live and deleted checkouts for each test, then truncates the checkout, customer, and organization tables with identity reset and cascade during teardown.
  • observed — Modified behavior in internal/store/postgres/billing_checkout_repository_pg_test.go: Adds helpers to execute SQL, retrieve the seeded customer and checkout IDs, and insert pending checkouts.
  • observed — Modified behavior in internal/store/postgres/billing_checkout_repository_pg_test.go: Adds a GetByID test expecting the live checkout to be returned and the soft-deleted checkout to produce checkout.ErrNotFound.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coveralls

coveralls commented Sep 30, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 36848031156

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage increased (+0.5%) to 54.262%

Details

  • Coverage increased (+0.5%) from the base build.
  • Patch coverage: 10 of 10 lines across 3 files are fully covered (100%).
  • 88 coverage regressions across 2 files.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

88 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
internal/store/postgres/user_repository.go 84 70.68%
pkg/auditrecord/consts.go 4 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 41322
Covered Lines: 22422
Line Coverage: 54.26%
Coverage Strength: 17.56 hits per line

💛 - Coveralls

@rohilsurana
rohilsurana force-pushed the soft-delete-billing-subscriptions-checkouts branch from ec2e29d to d568781 Compare October 1, 2026 07:05
@rohilsurana

Copy link
Copy Markdown
Member Author

End to end check on a local sandbox

This branch is rebased on main at 926c39e, which already has #1962. I ran this branch and main against the same local Frontier stack: the same Postgres and SpiceDB, the same seed, and the same 31 scenarios, run through the API as the bootstrap superuser. Only the binary changed between runs.

This branch: 31 of 31 pass. main: 25 of 31 pass.

New in this PR: subscriptions and checkouts

# Scenario Expected This branch main
23 Control: read a live subscription found PASS PASS
24 A soft-deleted subscription is not found not_found PASS FAIL (got found)
25 Subscription list skips the soft-deleted subscription 1 PASS FAIL (got 2)
26 Control: a live subscription grants the feature true PASS PASS
27 A soft-deleted subscription no longer grants the feature false PASS FAIL (got true)
28 Subscription list of an org whose only subscription is soft-deleted 0 PASS FAIL (got 1)
29 Control: read a live checkout found PASS PASS
30 A soft-deleted checkout is not found not_found PASS FAIL (got found)
31 Checkout list skips the soft-deleted checkout 1 PASS FAIL (got 2)

The three controls pass on both builds, so live rows behave the same. The other six fail on main because it still returns soft-deleted rows. The one that matters most is scenario 27: on main, an org whose only subscription is soft-deleted is still entitled to the feature. On this branch it is not.

Regression check: the 22 scenarios from #1962 (all pass on both builds)
# Scenario Expected This branch main
1 Control: read a live billing account found PASS PASS
2 Control: balance of an account with only live rows 40 PASS PASS
3 Control: list transactions of that account 2 PASS PASS
4 Control: list invoices of that account 1 PASS PASS
5 Control: list billing accounts of a live org 1 PASS PASS
6 A soft-deleted billing account is not found not_found PASS PASS
7 Billing details of a soft-deleted account are not found not_found PASS PASS
8 A soft-deleted account is left out of the org's account list 0 PASS PASS
9 Credit entitlement check on a soft-deleted account is not found not_found PASS PASS
10 Balance ignores soft-deleted transactions 70 PASS PASS
11 Transaction list ignores soft-deleted transactions 2 PASS PASS
12 Total debited ignores soft-deleted transactions 30 PASS PASS
13 Credit entitlement for 90 agrees with the balance of 70 false PASS PASS
14 Org invoice list skips the soft-deleted invoice 1 PASS PASS
15 Invoices of a soft-deleted account are not listed for the org 0 PASS PASS
16 Admin search shows live invoices present PASS PASS
17 Admin search hides a soft-deleted invoice hidden PASS PASS
18 Admin search hides an invoice of a soft-deleted account hidden PASS PASS
19 Admin search hides an invoice of a soft-deleted org hidden PASS PASS
20 Spending 90 against a balance of 70 is refused invalid_argument PASS PASS
21 Spending against a soft-deleted account is refused not_found PASS PASS
22 Control: spending within the balance still works ok/30 PASS PASS

These pass on main because #1962 has merged. They also pass on this branch, so the two changes work together.

How the data was set up:

  • Nothing writes deleted_at on these tables yet, so I set it with SQL.
  • The sandbox has no billing provider, so billing accounts, transactions, invoices, subscriptions, checkouts and the plan, product and feature were inserted with SQL.
  • The seed was reset before each build, because the spending scenarios write.

Not covered: anything that writes deleted_at, since that is outside this PR.

@rohilsurana
rohilsurana marked this pull request as ready for review October 1, 2026 07:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/store/postgres/billing_checkout_repository.go (1)

239-240: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

GetByName queries a name column that the table does not have.

The PR description says billing_checkouts has no name column. This method fails with a DB error on every call. The fromLive change does not alter that. The PR description says the method is unreachable, so the risk is low. Remove the method, or add a comment that marks it as dead code.

internal/store/postgres/billing_subscription_repository.go (1)

268-268: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

GetByName stays broken, but this change does not make it worse.

The billing_subscriptions table has no name column, according to the PR description. The query fails with a database error before the new fromLive filter has any effect. The PR notes this method is unreachable. Consider removing it or tracking it in a follow-up.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: raystack/frontier/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 655282f2-5f7b-42ef-948f-de47f78dbc64

📥 Commits

Reviewing files that changed from the base of the PR and between 926c39e and d568781.

📒 Files selected for processing (4)
  • internal/store/postgres/billing_checkout_repository.go
  • internal/store/postgres/billing_checkout_repository_pg_test.go
  • internal/store/postgres/billing_subscription_repository.go
  • internal/store/postgres/billing_subscription_repository_pg_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

@AmanGIT07 AmanGIT07 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The org delete reads subscriptions and checkouts through these filtered methods (core/deleter/service.go:407, :418), then hard-deletes. Once a row has deleted_at, the subscription is left behind and blocks the customer delete on the foreign key. The checkout is removed without its audit record. Nothing sets deleted_at yet, so this is fine today. Can we add a TODO(fix) next to the one at :410, so the PR that makes this delete soft picks it up?

@rohilsurana

Copy link
Copy Markdown
Member Author

Thanks for the review. Two follow-ups are pushed.

  • Added a TODO(fix) in core/deleter/service.go above the subscription delete. It says the subscription and checkout reads skip deleted_at rows, so those deletes must turn soft in the same change.
  • Removed the unused GetByName from the subscription and checkout repositories. No code calls them, and each one queries a name column that does not exist.

@rohilsurana
rohilsurana merged commit bb329ac into main Oct 1, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the soft-delete-billing-subscriptions-checkouts branch October 1, 2026 10:32

This branch was successfully deployed

1 active deployment
Preview — d2efb054 Deployed Oct 1, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants